﻿using System;
using System.Collections.Generic;
using System.Linq;
using Agatha.Common;
using Agatha.Common.InversionOfControl;
using Agatha.ServiceLayer;
using QuickNet;
using QuickNet.Generators;
using QuickNet.Generators.Meta;
using Rhino.Mocks;
using xunit.extensions.quicknet;

namespace Tests
{
    public class RequestProcessorSpecs : AcidTest
    {
        #region Context

        // ---------------------------
        // Todo make this dynamic
        
        public class FirstRequest : Request { }
        public class FirstResponse : Response { }

        private static IRequestHandler<FirstRequest> handlerForFirstRequest;

        public class SecondRequest : Request { }
        public class SecondResponse : Response { }

        private static IRequestHandler<SecondRequest> handlerForSecondRequest;

        public class ThirdRequest : Request { }
        public class ThirdResponse : Response { }

        private static IRequestHandler<ThirdRequest> handlerForThirdRequest;
        
        // ---------------------------

        private static IRequestProcessor requestProcessor;
        private static ServiceLayerConfiguration serviceLayerConfiguration;

        private static List<Exception> exceptionsThrown;

        public RequestProcessorSpecs() : base(10, 10) { }

        public override void SetUp()
        {
            IoC.Container = new Agatha.Castle.Container();

            serviceLayerConfiguration = MockRepository.GenerateStub<ServiceLayerConfiguration>(null, null, IoC.Container);
            serviceLayerConfiguration.BusinessExceptionType = typeof (BusinessException);
            serviceLayerConfiguration.SecurityExceptionType = typeof(SecurityException);

            requestProcessor = new RequestProcessor(serviceLayerConfiguration);

            handlerForFirstRequest = MockRepository.GenerateMock<IRequestHandler<FirstRequest>>();
            handlerForSecondRequest = MockRepository.GenerateMock<IRequestHandler<SecondRequest>>();
            handlerForThirdRequest = MockRepository.GenerateMock<IRequestHandler<ThirdRequest>>();

            IoC.Container.RegisterInstance(handlerForFirstRequest);
            IoC.Container.RegisterInstance(handlerForSecondRequest);
            IoC.Container.RegisterInstance(handlerForThirdRequest);
        }

        public class BusinessException : Exception { }
        public class SecurityException : Exception { }
        public class UnknownException : Exception { }
        public class AnotherUnknownException : Exception { }

        public class ProcessInputElement
        {
            public IRequestHandler RequestHandler { get; private set; }

            public Request Request { get; private set; }
            public Response Response { get; private set; }

            public ProcessInputElement(IRequestHandler requestHandler, Request request, Response response)
            {
                RequestHandler = requestHandler;
                Request = request;
                Response = response;
            }

            public void StubIt(Action<IList<Exception>> action, IList<Exception> exceptionsThrown)
            {
                RequestHandler
                    .Stub<IRequestHandler>(r => r.Handle(Request))
                    .Return(Response)
                    .WhenCalled(arg => action(exceptionsThrown))
                    .Repeat.Once();

                RequestHandler
                    .Stub<IRequestHandler>(r => r.CreateDefaultResponse())
                    .Return(Response)
                    .Repeat.Once();
            }
        }

        public class RequestsAndHandlers : Dictionary<Type, ProcessInputElement>
        {
            public RequestsAndHandlers()
            {
                Add(typeof(FirstRequest),
                    new ProcessInputElement(handlerForFirstRequest, new FirstRequest(), new FirstResponse()));
                Add(typeof(SecondRequest),
                    new ProcessInputElement(handlerForSecondRequest, new SecondRequest(), new SecondResponse()));
                Add(typeof(ThirdRequest),
                    new ProcessInputElement(handlerForThirdRequest, new ThirdRequest(), new ThirdResponse()));
            }
        }

        public class ProcessInput
        {
            public IList<Action<IList<Exception>>> Actions { get; set; }
            public RequestsAndHandlers RequestsAndHandlers { get; set; }
            public Request[] Requests
            {
                get
                {
                    return RequestsAndHandlers.ToList().ConvertAll(kv => kv.Value.Request).ToArray();
                }
            }
        }

        public class ProcessInputGenerator : BaseGenerator<ProcessInput>
        {
            protected override ProcessInput GetDefaultInstance()
            {
                return new ProcessInput
                    {
                        RequestsAndHandlers = new RequestsAndHandlers()
                    };
            }

            public ProcessInputGenerator()
            {
                IGenerator<Action<IList<Exception>>> actionGenerator =
                    new ChoiceGenerator<Action<IList<Exception>>>(
                        new Action<IList<Exception>>[]
                            {
                                // repetition increases likelyhood
                                l => l.Add(null),
                                l => l.Add(null),
                                l => l.Add(null),
                                l => l.Add(null),
                                l =>
                                    {
                                        Exception exception = new BusinessException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new SecurityException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new UnknownException();
                                        l.Add(exception);
                                        throw exception;
                                    },
                                l =>
                                    {
                                        Exception exception = new AnotherUnknownException();
                                        l.Add(exception);
                                        throw exception;
                                    }
                            });
                AddGeneratorForProperty(
                    t => t.Actions,
                    new ListGenerator<Action<IList<Exception>>>(actionGenerator, 3, 3));
            }
        }
        #endregion

        #region Transitions 

        class Process : MetaTransition<ProcessInput, Response[]>
        {
            public Process()
            {
                Generator = new ProcessInputGenerator();
                Execute = 
                    input =>
                        {
                            int ix = 0;
                            exceptionsThrown = new List<Exception>();
                            foreach (Type key in input.RequestsAndHandlers.Keys)
                            {
                                input.RequestsAndHandlers[key].StubIt(input.Actions[ix], exceptionsThrown);
                                ix++;
                            }
                            return requestProcessor.Process(input.Requests);
                        };
                
            }
        }

        #endregion

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithoutException(ProcessInput input, Response[] output)
        {
            return new Spec(
                () => output.ForEach(
                          r => Ensure.Equal(ExceptionType.None, r.ExceptionType)))
                .IfAfter(() => !exceptionsThrown.Exists(e => e != null));
        }

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithBusinessException(ProcessInput input, Response[] output)
        {
            Predicate<Exception> predicate = 
                exception => exception != null && exception.GetType() == typeof(BusinessException);

            return new Spec(
                () =>
                    {
                        int ix = exceptionsThrown.FindIndex(predicate);
                        Ensure.Equal(ExceptionType.Business, output[ix].ExceptionType);

                        Ensure.Equal(exceptionsThrown[ix].Message, output[ix].Exception.Message);
                        Ensure.Equal(exceptionsThrown[ix].StackTrace, output[ix].Exception.StackTrace);
                        Ensure.Equal("Tests.RequestProcessorSpecs+BusinessException", output[ix].Exception.Type);
                    })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithSecurityException(ProcessInput input, Response[] output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(SecurityException);

            return new Spec(
                () =>
                {
                    int ix = exceptionsThrown.FindIndex(predicate);
                    Ensure.Equal(ExceptionType.Security, output[ix].ExceptionType);

                    Ensure.Equal(exceptionsThrown[ix].Message, output[ix].Exception.Message);
                    Ensure.Equal(exceptionsThrown[ix].StackTrace, output[ix].Exception.StackTrace);
                    Ensure.Equal("Tests.RequestProcessorSpecs+SecurityException", output[ix].Exception.Type);
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithUnknownException(ProcessInput input, Response[] output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(UnknownException);
            return new Spec(
                () =>
                {
                    int ix = exceptionsThrown.FindIndex(predicate);
                    Ensure.Equal(ExceptionType.Unknown, output[ix].ExceptionType);

                    Ensure.Equal(exceptionsThrown[ix].Message, output[ix].Exception.Message);
                    Ensure.Equal(exceptionsThrown[ix].StackTrace, output[ix].Exception.StackTrace);
                    Ensure.Equal("Tests.RequestProcessorSpecs+UnknownException", output[ix].Exception.Type);
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithAnotherUnknownException(ProcessInput input, Response[] output)
        {
            Predicate<Exception> predicate =
                exception => exception != null && exception.GetType() == typeof(AnotherUnknownException);
            return new Spec(
                () =>
                {
                    int ix = exceptionsThrown.FindIndex(predicate);
                    Ensure.Equal(ExceptionType.Unknown, output[ix].ExceptionType);

                    Ensure.Equal(exceptionsThrown[ix].Message, output[ix].Exception.Message);
                    Ensure.Equal(exceptionsThrown[ix].StackTrace, output[ix].Exception.StackTrace);
                    Ensure.Equal("Tests.RequestProcessorSpecs+AnotherUnknownException", output[ix].Exception.Type);
                })
                .IfAfter(() => exceptionsThrown.Exists(predicate));
        }

        [SpecFor(typeof(Process))]
        public Spec ProcessRequestsWithExceptionFailsAllFollowingRequests(ProcessInput input, Response[] output)
        {
            Predicate<Exception> predicate =
                exception => exception != null;
            return new Spec(
                () =>
                    {
                        int ix = exceptionsThrown.FindIndex(predicate) + 1;
                        for (int i = ix; i < output.Length; i++)
                        {
                            Ensure.Equal(ExceptionType.EarlierRequestAlreadyFailed, output[ix].ExceptionType);

                            Ensure.Equal("EarlierRequestAlreadyFailed", output[ix].Exception.Message);
                            Ensure.Null(output[ix].Exception.StackTrace);
                            Ensure.Equal("System.Exception", output[ix].Exception.Type);
                        }
                    })
                .IfAfter(
                () =>    exceptionsThrown.FindIndex(predicate) >= 0
                      && exceptionsThrown.FindIndex(predicate) >= exceptionsThrown.Count - 1); 
                         // excluding cases where only last request fails
        }
    }
}
